package net.borderwars.terrain;

import java.awt.*;
import java.util.Random;

/**
 * @author ehubbard
 *         Date: Aug 22, 2005
 *         Time: 7:14:21 PM
 */
public class FractalTerrain {
    private double[][] terrain;
    private double min;
    private double max;
    private int divisions;
    private Random rng;

    private static double roughness = .7;
    private boolean startSet;

    public FractalTerrain () {
        this (8, roughness, Double.NaN);
    }

    public FractalTerrain (double start) {
        this (8, roughness, start);
    }

    public FractalTerrain (double start, double roughness) {
        this (8, roughness, start);
    }

    public FractalTerrain (int lod, double roughness, double start) {
        startSet = (Double.NaN == start);
        this.divisions = 1 << lod;
        terrain = new double[divisions + 1][divisions + 1];
        rng = new Random ();

        if (!startSet) {
            terrain[0][0] = rnd ();
            terrain[0][divisions] = rnd ();
            terrain[divisions][divisions] = rnd ();
            terrain[divisions][0] = rnd ();
        }
        else {
            terrain[0][0] = start;
            terrain[0][divisions] = start;
            terrain[divisions][divisions] = start;
            terrain[divisions][0] = start;
        }

        double rough = roughness;
        for (int i = 0; i < lod; ++i) {
            int q = 1 << i, r = 1 << (lod - i), s = r >> 1;
            for (int j = 0; j < divisions; j += r) {
                for (int k = 0; k < divisions; k += r) {
                    diamond (j, k, r, rough);
                }
            }
            if (s > 0) {
                for (int j = 0; j <= divisions; j += s) {
                    for (int k = (j + s) % r; k <= divisions; k += r) {
                        square (j - s, k - s, r, rough);
                    }
                }
            }
            rough *= roughness;
        }

        min = max = terrain[0][0];
        for (int i = 0; i <= divisions; ++i) {
            for (int j = 0; j <= divisions; ++j) {
                if (terrain[i][j] < min) {
                    min = terrain[i][j];
                }
                else
                    if (terrain[i][j] > max) {
                        max = terrain[i][j];
                    }
            }
        }
    }

    private void diamond (int x, int y, int side, double scale) {
        if (side > 1) {
            int half = side / 2;
            double avg = (terrain[x][y] + terrain[x + side][y] + terrain[x + side][y + side] + terrain[x][y + side]) * 0.25;
            terrain[x + half][y + half] = avg + rnd () * scale;
        }
    }

    private void square (int x, int y, int side, double scale) {
        int half = side / 2;
        double avg = 0.0, sum = 0.0;
        if (x >= 0) {
            avg += terrain[x][y + half];
            sum += 1.0;
        }
        if (y >= 0) {
            avg += terrain[x + half][y];
            sum += 1.0;
        }
        if (x + side <= divisions) {
            avg += terrain[x + side][y + half];
            sum += 1.0;
        }
        if (y + side <= divisions) {
            avg += terrain[x + half][y + side];
            sum += 1.0;
        }
        terrain[x + half][y + half] = avg / sum + rnd () * scale;
    }

    private double rnd () {
        return 2. * rng.nextDouble () - 1.0;
    }

    public double getAltitude (int i, int j) {
        double alt = terrain[(int) i][(int) j];
        return (alt - min) / (max - min);

    }

    public double getAltitude (double i, double j) {
        double alt = terrain[(int) (i * divisions)][(int) (j * divisions)];
        if (!startSet) {
            return (alt - min) / (max - min);
        }
        else {
            return (alt);
        }
    }

    private RGB blue = new RGB (0.0, 0.0, 1.0);
    private RGB green = new RGB (0.0, 1.0, 0.0);
    private RGB white = new RGB (1.0, 1.0, 1.0);

    public RGB getColor (int i, int j) {
        double a = getAltitude (i, j);
        if (a > .90) {        // mountain area make it white
            return (white.scale (a));
        }
        else
            if (a > .85) {
                return (white.scale (a));
            }
            else
                if (a > .6) {
                    return (green.scale (a).add (green.scale (.05)));
                }
                else {
                    return (blue.scale (a).add (blue.scale (.3)));
                }

//        if (a < .5)
//           return blue.add(green.subtract(blue).scale((a - 0.0) / 0.5));
//        else
//            return green.add(white.subtract(green).scale((a - 0.5) / 0.5));

    }

    public RGB getColor (double i, double j) {
        double a = getAltitude (i, j);
        if (a < .5) {
            return blue.add (green.subtract (blue).scale ((a - 0.0) / 0.5));
        }
        else {
            return green.add (white.subtract (green).scale ((a - 0.5) / 0.5));
        }
    }

    public class RGB {
        private double r,
                g,
                b;

        public Color getColor () {
            int ir = (int) (r * 256);
            int ib = (int) (b * 256);
            int ig = (int) (g * 256);
            return new Color (Math.min (ir, 255), Math.min (ig, 255), Math.min (ib, 255));
        }

        public RGB (double r, double g, double b) {
            this.r = r;
            this.g = g;
            this.b = b;
        }

        public RGB add (RGB rgb) {
            return new RGB (r + rgb.r, g + rgb.g, b + rgb.b);
        }

        public RGB subtract (RGB rgb) {
            return new RGB (r - rgb.r, g - rgb.g, b - rgb.b);
        }

        public RGB scale (double scale) {
            return new RGB (r * scale, g * scale, b * scale);
        }

        private int toInt (double value) {
            int v = (int) (value * 255.5);
            return (v < 0) ? 0 : (v > 255) ? 255 : v;
        }

        public int toRGB () {
            return (0xff << 24) | (toInt (r) << 16) | (toInt (g) << 8) | toInt (b);
        }
    }
}
